import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import uuid
import shutil
import threading
import pathlib
from datetime import datetime
import random

class FileBackupTool:
    def __init__(self, root):
        self.root = root
        self.root.title("\u6587\u4ef6\u5907\u4efd\u5de5\u5177")
        # self.root.geometry("900x650")  # \u7f29\u5c0f\u7a97\u53e3\u9ad8度
        self.root.resizable(True, True)

        self.root.option_add("*Font", "SimHei 10")

        self.main_frame = ttk.Frame(root, padding="10")  # \u7f29\u5c0fpadding
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        self.style = ttk.Style()
        self.style.configure("Header.TLabel", font=("SimHei", 12, "bold"))
        self.style.configure("Section.TFrame", relief="ridge", borderwidth=1)
        self.style.configure("TButton", padding=5)
        self.style.configure("TRadiobutton", padding=2)

        # ==================== \u5fc5\u8981\u6587\u4ef6\u5939\u533a\u57df ====================
        ttk.Label(self.main_frame, text="\u5fc5\u8981\u6587\u4ef6\u5939\u8bbe\u7f6e", style="Header.TLabel").grid(
            row=0, column=0, columnspan=3, sticky=tk.W, pady=(0, 10)
        )
        req_frame = ttk.Frame(self.main_frame, style="Section.TFrame", padding=8)
        req_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))

        ttk.Label(req_frame, text="\u6587\u4ef6\u5939\u8def\u5f84:").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.required_dir_var = tk.StringVar()
        ttk.Entry(req_frame, textvariable=self.required_dir_var, width=60).grid(row=0, column=1, pady=2, padx=(5, 10))
        ttk.Button(req_frame, text="\u6d4f\u89c8...", command=self.select_required_dir).grid(row=0, column=2, pady=2)

        ttk.Label(req_frame, text="\u5305\u542b\u7684\u5b50\u76ee\u5f55:").grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(10, 2))
        self.req_tree = ttk.Treeview(req_frame, columns=("1", "2"), show="headings", height=3)
        self.req_tree.heading("1", text="\u6587\u4ef6\u5939\u540d\u79f0")
        self.req_tree.heading("2", text="\u4fee\u6539\u65f6\u95f4")
        self.req_tree.column("1", width=350)
        self.req_tree.column("2", width=180)
        scrollbar1 = ttk.Scrollbar(req_frame, orient=tk.VERTICAL, command=self.req_tree.yview)
        self.req_tree.configure(yscroll=scrollbar1.set)
        self.req_tree.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=2)
        scrollbar1.grid(row=2, column=2, sticky=(tk.N, tk.S))
        req_frame.columnconfigure(1, weight=1)

        # ==================== \u53ef\u9009\u6587\u4ef6\u5939 ====================
        ttk.Label(self.main_frame, text="\u53ef\u9009\u6587\u4ef6\u5939\u8bbe\u7f6e", style="Header.TLabel").grid(
            row=2, column=0, columnspan=3, sticky=tk.W, pady=(10, 10)
        )
        opt_frame = ttk.Frame(self.main_frame, style="Section.TFrame", padding=8)
        opt_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))

        ttk.Label(opt_frame, text="\u6587\u4ef6\u5939\u8def\u5f84:").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.optional_dir_var = tk.StringVar()
        ttk.Entry(opt_frame, textvariable=self.optional_dir_var, width=60).grid(row=0, column=1, pady=2, padx=(5, 10))
        ttk.Button(opt_frame, text="\u6d4f\u89c8...", command=self.select_optional_dir).grid(row=0, column=2, pady=2)

        ttk.Label(opt_frame, text="\u7b5b\u9009\u9009\u9879:").grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(10, 2))
        self.optional_option = tk.StringVar(value="all")
        filter_frame = ttk.Frame(opt_frame)
        filter_frame.grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=2)

        ttk.Radiobutton(filter_frame, text="\u5168\u90e8\u5907\u4efd", variable=self.optional_option, value="all").grid(row=0, column=0, sticky=tk.W, padx=5)
        ttk.Radiobutton(filter_frame, text="\u5168\u90e8\u4e0d\u5907\u4efd", variable=self.optional_option, value="none").grid(row=1, column=0, sticky=tk.W, padx=5)

        ttk.Radiobutton(filter_frame, text="\u524d N \u4e2a\u6587\u4ef6\u5939", variable=self.optional_option, value="first_n").grid(row=0, column=1, sticky=tk.W, padx=5)
        self.first_n_var = tk.IntVar(value=10)
        ttk.Entry(filter_frame, textvariable=self.first_n_var, width=5).grid(row=0, column=2)

        ttk.Radiobutton(filter_frame, text="\u540e N \u4e2a\u6587\u4ef6\u5939", variable=self.optional_option, value="last_n").grid(row=1, column=1, sticky=tk.W, padx=5)
        self.last_n_var = tk.IntVar(value=10)
        ttk.Entry(filter_frame, textvariable=self.last_n_var, width=5).grid(row=1, column=2)

        ttk.Radiobutton(filter_frame, text="\u968f\u673a\u9009\u62e9 N \u4e2a", variable=self.optional_option, value="random_n").grid(row=0, column=3, sticky=tk.W, padx=5)
        self.random_n_var = tk.IntVar(value=10)
        ttk.Entry(filter_frame, textvariable=self.random_n_var, width=5).grid(row=0, column=4)

        ttk.Label(opt_frame, text="\u5305\u542b\u7684\u5b50\u76ee\u5f55:").grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=(10, 2))
        self.opt_tree = ttk.Treeview(opt_frame, columns=("1", "2"), show="headings", height=3)
        self.opt_tree.heading("1", text="\u6587\u4ef6\u5939\u540d\u79f0")
        self.opt_tree.heading("2", text="\u4fee\u6539\u65f6\u95f4")
        self.opt_tree.column("1", width=350)
        self.opt_tree.column("2", width=180)
        scrollbar2 = ttk.Scrollbar(opt_frame, orient=tk.VERTICAL, command=self.opt_tree.yview)
        self.opt_tree.configure(yscroll=scrollbar2.set)
        self.opt_tree.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=2)
        scrollbar2.grid(row=4, column=2, sticky=(tk.N, tk.S))
        opt_frame.columnconfigure(1, weight=1)

        # ==================== \u5907\u4efd\u9009\u9879 ====================
        ttk.Label(self.main_frame, text="\u5907\u4efd\u9009\u9879\u8bbe\u7f6e", style="Header.TLabel").grid(
            row=4, column=0, columnspan=3, sticky=tk.W, pady=(10, 5)
        )
        options_frame = ttk.Frame(self.main_frame, style="Section.TFrame", padding=8)
        options_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))

        self.use_uuid_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(options_frame, text="\u4f7f\u75288\u4f4dUUID\u547d\u540d\u5907\u4efd\u6587\u4ef6\u5939", variable=self.use_uuid_var).grid(row=0, column=0, sticky=tk.W, pady=4, padx=5)
        self.shuffle_order_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(options_frame, text="\u6253\u4e71\u5907\u4efd\u987a\u5e8f", variable=self.shuffle_order_var).grid(row=1, column=0, sticky=tk.W, pady=4, padx=5)

        ttk.Label(options_frame, text="\u5907\u4efd\u4fdd\u5b58\u8def\u5f84:").grid(row=2, column=0, sticky=tk.W, pady=(10, 2))
        self.save_path_var = tk.StringVar()
        ttk.Entry(options_frame, textvariable=self.save_path_var, width=60).grid(row=3, column=0, pady=2, padx=5)
        ttk.Button(options_frame, text="\u6d4f\u89c8...", command=self.select_save_folder).grid(row=3, column=1, pady=2, padx=5)
        options_frame.columnconfigure(0, weight=1)

        # ==================== \u64cd\u4f5c\u533a ====================
        control_frame = ttk.Frame(self.main_frame)
        control_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)

        ttk.Button(control_frame, text="\u5f00\u59cb\u5907\u4efd", command=self.start_backup, style="TButton", padding=(20, 5)).grid(row=0, column=0, pady=5)
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(control_frame, variable=self.progress_var, length=100, mode="determinate")
        self.progress_bar.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=2)

        ttk.Label(control_frame, text="\u5907\u4efd\u65e5\u5fd7:").grid(row=2, column=0, sticky=tk.W, pady=(10, 2))
        self.log_text = tk.Text(control_frame, height=4, width=80, wrap=tk.WORD)
        self.log_text.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=2)
        scrollbar_log = ttk.Scrollbar(control_frame, orient=tk.VERTICAL, command=self.log_text.yview)
        self.log_text.configure(yscroll=scrollbar_log.set)
        scrollbar_log.grid(row=3, column=1, sticky=(tk.N, tk.S))

        control_frame.columnconfigure(0, weight=1)
        control_frame.rowconfigure(3, weight=1)
        self.main_frame.columnconfigure(0, weight=1)

        # ... 其余函数与备份逻辑不变，可复用你已有代码 ...
        # 初始化变量
        self.required_folders = []  # 必要文件夹的所有子目录
        self.optional_folders = []  # 可选文件夹的所有子目录
        self.backup_list = []       # 最终待备份的目录列表
        
        # 记录开始时间
        self.start_time = None
    
    def log(self, message):
        """向日志区域添加消息"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
        self.log_text.see(tk.END)
    
    # ==================== 必要文件夹处理 ====================
    def select_required_dir(self):
        """选择必要文件夹"""
        folder_path = filedialog.askdirectory(title="选择必要文件夹（所有子目录将被备份）")
        if folder_path:
            self.required_dir_var.set(folder_path)
            self.load_required_dir_folders()
    
    def load_required_dir_folders(self):
        """加载并显示必要文件夹的所有子目录"""
        self.log("正在加载必要文件夹的子目录...")
        self.required_folders = []
        
        # 清空现有内容
        for item in self.req_tree.get_children():
            self.req_tree.delete(item)
        
        req_path = pathlib.Path(self.required_dir_var.get())
        if not req_path.exists() or not req_path.is_dir():
            self.log("错误: 必要文件夹不存在或不是一个目录")
            return
        
        try:
            # 获取所有子文件夹并按名称排序
            self.required_folders = sorted(
                [f for f in req_path.iterdir() if f.is_dir()],
                key=lambda x: x.name
            )
            
            # 显示文件夹信息
            for folder in self.required_folders:
                try:
                    stats = folder.stat()
                    modified = datetime.fromtimestamp(stats.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
                    self.req_tree.insert("", tk.END, values=(folder.name, modified))
                except Exception as e:
                    self.log(f"无法获取文件夹 {folder.name} 的信息: {str(e)}")
            
            self.log(f"必要文件夹共加载 {len(self.required_folders)} 个子目录（将全部备份）")
        except Exception as e:
            self.log(f"加载必要文件夹内容时出错: {str(e)}")
    
    # ==================== 可选文件夹处理 ====================
    def select_optional_dir(self):
        """选择可选文件夹"""
        folder_path = filedialog.askdirectory(title="选择可选文件夹（可选择子目录）")
        if folder_path:
            self.optional_dir_var.set(folder_path)
            self.load_optional_dir_folders()
    
    def load_optional_dir_folders(self):
        """加载并显示可选文件夹的所有子目录"""
        self.log("正在加载可选文件夹的子目录...")
        self.optional_folders = []
        
        # 清空现有内容
        for item in self.opt_tree.get_children():
            self.opt_tree.delete(item)
        
        opt_path = pathlib.Path(self.optional_dir_var.get())
        if not opt_path.exists() or not opt_path.is_dir():
            self.log("错误: 可选文件夹不存在或不是一个目录")
            return
        
        try:
            # 获取所有子文件夹并按名称排序
            self.optional_folders = sorted(
                [f for f in opt_path.iterdir() if f.is_dir()],
                key=lambda x: x.name
            )
            
            # 显示文件夹信息
            for folder in self.optional_folders:
                try:
                    stats = folder.stat()
                    modified = datetime.fromtimestamp(stats.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
                    self.opt_tree.insert("", tk.END, values=(folder.name, modified))
                except Exception as e:
                    self.log(f"无法获取文件夹 {folder.name} 的信息: {str(e)}")
            
            self.log(f"可选文件夹共加载 {len(self.optional_folders)} 个子目录（可筛选备份）")
        except Exception as e:
            self.log(f"加载可选文件夹内容时出错: {str(e)}")
    
    # ==================== 保存路径选择 ====================
    def select_save_folder(self):
        """选择保存路径"""
        folder_path = filedialog.askdirectory(title="选择备份保存路径")
        if folder_path:
            self.save_path_var.set(folder_path)
    
    # ==================== 输入验证 ====================
    def validate_inputs(self):
        """验证输入是否有效"""
        req_path = self.required_dir_var.get()
        save_path = self.save_path_var.get()
        
        # 验证必要文件夹
        if not req_path or not pathlib.Path(req_path).exists():
            messagebox.showerror("错误", "请选择有效的必要文件夹")
            return False
        
        if not self.required_folders:
            messagebox.showerror("错误", "必要文件夹中没有可备份的子目录")
            return False
        
        # 验证保存路径
        if not save_path:
            messagebox.showerror("错误", "请选择备份保存路径")
            return False
        
        # 可选文件夹可选，如果选择了则验证
        opt_path = self.optional_dir_var.get()
        if opt_path and not pathlib.Path(opt_path).exists():
            messagebox.showerror("错误", "请选择有效的可选文件夹")
            return False
        
        return True
    
    # ==================== 筛选可选文件夹 ====================
    def filter_optional_folders(self):
        """根据选择筛选可选文件夹的子目录"""
        option = self.optional_option.get()
        filtered = []
        total = len(self.optional_folders)
        
        if not self.optional_folders:
            self.log("可选文件夹中没有子目录，不进行备份")
            return filtered
            
        if option == "all":
            filtered = self.optional_folders
            self.log(f"可选文件夹选择全部 {total} 个子目录")
            
        elif option == "none":
            self.log("可选文件夹选择全部不备份")
            
        elif option == "first_n":
            n = self.first_n_var.get()
            n = min(n, total)  # 防止n大于实际数量
            filtered = self.optional_folders[:n]
            self.log(f"可选文件夹选择前 {n} 个子目录")
            
        elif option == "last_n":
            n = self.last_n_var.get()
            n = min(n, total)  # 防止n大于实际数量
            filtered = self.optional_folders[-n:]
            self.log(f"可选文件夹选择后 {n} 个子目录")
            
        elif option == "random_n":
            n = self.random_n_var.get()
            n = min(n, total)  # 防止n大于实际数量
            filtered = random.sample(self.optional_folders, n)
            self.log(f"可选文件夹随机选择 {n} 个子目录")
        
        return filtered
    
    # ==================== 生成8位UUID ====================
    def generate_short_uuid(self):
        """生成8位长度的UUID字符串"""
        full_uuid = uuid.uuid4().hex
        return full_uuid[:8]
    
    # ==================== 开始备份 ====================
    def start_backup(self):
        """开始备份文件夹"""
        if not self.validate_inputs():
            return
        
        # 构建待备份列表：必要文件夹全部 + 可选文件夹筛选结果
        self.backup_list = self.required_folders.copy()
        optional_filtered = self.filter_optional_folders()
        self.backup_list.extend(optional_filtered)
        
        total_count = len(self.backup_list)
        if total_count == 0:
            messagebox.showinfo("提示", "没有需要备份的文件夹")
            return
        
        # 如果启用了打乱顺序，随机排序备份列表
        if self.shuffle_order_var.get():
            random.shuffle(self.backup_list)
            self.log("已打乱备份文件夹顺序")
        
        # 确认对话框
        use_uuid_text = "使用8位UUID命名" if self.use_uuid_var.get() else "使用原始名称"
        shuffle_text = "打乱顺序" if self.shuffle_order_var.get() else "保持原顺序"
        result = messagebox.askyesno(
            "确认备份", 
            f"即将备份共 {total_count} 个文件夹\n"
            f"其中：必要文件夹 {len(self.required_folders)} 个，可选文件夹 {len(optional_filtered)} 个\n\n"
            f"备份命名方式: {use_uuid_text}\n"
            f"文件夹顺序: {shuffle_text}\n"
            f"保存路径: {self.save_path_var.get()}"
        )
        
        if not result:
            self.log("操作已取消")
            return
        
        # 清空日志和进度条
        self.log_text.delete(1.0, tk.END)
        self.progress_var.set(0)
        
        # 在新线程中执行备份，避免界面卡顿
        self.start_time = datetime.now()
        self.log(f"开始备份... 时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
        self.log(f"共需备份 {total_count} 个文件夹")
        
        thread = threading.Thread(target=self.perform_backup)
        thread.daemon = True
        thread.start()
    
    def perform_backup(self):
        """执行备份操作的主函数"""
        try:
            save_path = pathlib.Path(self.save_path_var.get())
            save_path.mkdir(parents=True, exist_ok=True)
            
            total_folders = len(self.backup_list)
            success_count = 0
            fail_count = 0
            
            for i, folder in enumerate(self.backup_list, 1):
                try:
                    # 更新进度条
                    progress = (i / total_folders) * 100
                    self.progress_var.set(progress)
                    self.root.update_idletasks()
                    
                    # 确定目标文件夹名称
                    if self.use_uuid_var.get():
                        target_name = self.generate_short_uuid()
                    else:
                        target_name = folder.name
                    
                    target_path = save_path / target_name
                    
                    # 执行复制操作
                    self.log(f"正在备份: {folder} -> {target_path}")
                    shutil.copytree(folder, target_path)
                    
                    self.log(f"成功备份: {folder.name} -> {target_name}")
                    success_count += 1
                    
                except Exception as e:
                    self.log(f"备份失败: {folder.name} - 错误: {str(e)}")
                    fail_count += 1
            
            # 备份完成
            end_time = datetime.now()
            duration = (end_time - self.start_time).total_seconds()
            
            self.log("=" * 50)
            self.log(f"备份完成! 总耗时: {duration:.2f} 秒")
            self.log(f"成功: {success_count} 个，失败: {fail_count} 个")
            self.log(f"备份文件保存位置: {save_path}")
            
            # 显示完成消息
            messagebox.showinfo(
                "备份完成", 
                f"备份完成!\n总耗时: {duration:.2f} 秒\n"
                f"成功: {success_count} 个，失败: {fail_count} 个"
            )
            
        except Exception as e:
            self.log(f"备份过程中发生严重错误: {str(e)}")
            messagebox.showerror("错误", f"备份过程中发生严重错误:\n{str(e)}")

if __name__ == "__main__":
    root = tk.Tk()
    app = FileBackupTool(root)
    root.mainloop()
